﻿using System;
using RaytracerLib.MathLib;
using YAXLib;
using System.Diagnostics;
using TomanuExtensions;
using RaytracerLib.Rays;
using System.Linq;
using System.Runtime.InteropServices;
using RaytracerLib.MathLib.Polynomials;
using RaytracerLib.UVMappers;

namespace RaytracerLib.RenderableObjects
{
    /// <summary>
    /// Torus lays in XZ plane. Y axis goes through its hole.
    /// </summary>
    public class TorusObject : RenderableObject
    {
        private double m_big_radius;
        private double m_small_radius;
        private double m_local_big_radius;
        private double m_local_small_radius;
        private PolynomialSolver m_solver;

        public TorusObject(Vector3 a_right, Vector3 a_up) :
            base(a_right, a_up)
        {
            Name = "Torus";
            m_uv_mapper = new TorodidalUVMapper();
            Closed = true;
            Update(UpdateFlags.All);
        }

        internal override Intersection GetIntersection(Intersection a_source_ray_intersection, Ray a_ray)
        {
            Vector3 local_dir;
            Vector3 local_start;
            bool back_hit = false;

            if (!TransformToLocalToBoundBox(a_ray, out local_dir, out local_start))
                return Scene.NoIntersection;

            double r2 = m_local_small_radius * m_local_small_radius;
            double R2 = m_local_big_radius * m_local_big_radius;
            double beta = 2 * local_dir * local_start;
            double gamma = local_start.SqrLen - r2 - R2;

            double c3 = 2 * beta;
            double c2 = beta * beta + 2 * gamma + 4 * R2 * local_dir.Y * local_dir.Y;
            double c1 = 2 * beta * gamma + 8 * R2 * local_start.Y * local_dir.Y;
            double c0 = gamma * gamma + 4 * R2 * local_start.Y * local_start.Y - 4 * R2 * r2;

            double dist = m_solver.SolveQuartic(c0, c1, c2, c3, 1);

            if (dist == Double.PositiveInfinity)
                return Scene.NoIntersection;

            Vector3 local_pos = local_start + local_dir * dist;

            Vector3 normal = (local_pos - 
                new Vector3(local_pos.X, 0, local_pos.Z).Normalized * m_local_big_radius).Normalized;

            back_hit = normal * local_dir > 0;

            if (back_hit && OneSide)
                return Scene.NoIntersection;

            Vector3 world_pos = LocalToWorld * local_pos;
            double world_dist = (world_pos - a_ray.Start).Length;

            Intersection intersection = new Intersection()
            {
                PrevIntersection = a_source_ray_intersection, 
                SceneObject = this,
                SourceRay = a_ray,
                LocalPos = local_pos,
                Dist = world_dist,
                Scene = Scene,
                BackHit = back_hit,
                Pos = world_pos
            };

            // Numerical errors.
            if (intersection.Normal * intersection.SourceRay.Dir >= 0)
                return Scene.NoIntersection;

            return intersection;
        }

        protected override Vector3 LocalScale
        {
            get
            {
                return Vector3.ONE * (BigRadius + SmallRadius);
            }
        }

        protected override AABB GetLocalBoundBox()
        {
            Vector3 v;
            if (BigRadius + SmallRadius != 0)
            {
                v = new Vector3(1, SmallRadius / (BigRadius + SmallRadius), 1);
                return new AABB(-v, v);
            }
            else
                return AABB.POINT; 
        }

        internal override Vector3 GetNormal(Intersection a_intersection)
        {
            Vector3 local_pos = a_intersection.LocalPos;
            
            if (a_intersection.BackHit)
            {
                return (LocalToWorldNormal * (new Vector3(local_pos.X, 0, local_pos.Z).Normalized *
                        m_local_big_radius - local_pos)).Normalized;
            }
            else
            {
                return (LocalToWorldNormal * (local_pos - new Vector3(local_pos.X, 0, local_pos.Z).Normalized *
                        m_local_big_radius)).Normalized;
            }
        }

        internal override Vector3 GetUVW(Intersection a_intersection)
        {
            double h = m_local_small_radius / (m_local_small_radius + m_local_big_radius);

            Vector3 uvw = base.GetUVW(a_intersection) / new Vector3(2, 2 * h, 2) + Vector3.HALF;

            Debug.Assert(DoubleExtensions.AlmostInRange(uvw.X, 0, 1, Constants.DOUBLE_PRECISION));
            Debug.Assert(DoubleExtensions.AlmostInRange(uvw.Y, 0, 1, Constants.DOUBLE_PRECISION));
            Debug.Assert(DoubleExtensions.AlmostInRange(uvw.Z, 0, 1, Constants.DOUBLE_PRECISION));

            uvw = new Vector3(
                DoubleExtensions.Limit(uvw.X, 0, 1),
                DoubleExtensions.Limit(uvw.Y, 0, 1),
                DoubleExtensions.Limit(uvw.Z, 0, 1));

            return uvw;
        }

        public override string ToString()
        {
            return String.Format("Torus: {0}, R: {1}, r: {2}, Pos: {3}", Name, BigRadius, SmallRadius, Pos);
        }

        internal override void GetTangents(Intersection a_intersection, out Vector3 a_tangent_x,
            out Vector3 a_tangent_y)
        {
            a_tangent_x = Vector3.CrossProduct(Up, a_intersection.Pos).Normalized;

            if (a_intersection.BackHit)
                a_tangent_y = Vector3.CrossProduct(a_intersection.Normal, a_tangent_x).Normalized;
            else
                a_tangent_y = Vector3.CrossProduct(a_tangent_x, a_intersection.Normal).Normalized;
        }

        [YAXNode]
        public double BigRadius
        {
            get
            {
                return m_big_radius;
            }
            set
            {
                m_big_radius = value;
                Update(UpdateFlags.BoundBox | UpdateFlags.Matrices | UpdateFlags.UVMapper);
            }
        }

        [YAXNode]
        public double SmallRadius
        {
            get
            {
                return m_small_radius;
            }
            set
            {
                m_small_radius = value;
                Update(UpdateFlags.BoundBox | UpdateFlags.Matrices | UpdateFlags.UVMapper);
            }
        }

        protected override void RenderStart(RenderStartPhase a_phase)
        {
            base.RenderStart(a_phase);

            if (a_phase == RenderStartPhase.PrepareObjectToRender)
            {
                m_solver = new PolynomialSolver(Scene.RenderOptions.PolynomialSolveMethod,
                    Scene.RenderOptions.RootFindingMethod);

                double ratio = m_big_radius / m_small_radius;
                m_local_small_radius = 1 / (ratio + 1);
                m_local_big_radius = ratio / (1 + ratio);
            }
        }

        protected override void Update(UpdateFlags a_flags)
        {
            base.Update(a_flags);

            if (a_flags.HasFlag(UpdateFlags.UVMapper))
            {
                if (m_uv_mapper is TorodidalUVMapper)
                {
                    (m_uv_mapper as TorodidalUVMapper).BigRadius = BigRadius;
                    (m_uv_mapper as TorodidalUVMapper).SmallRadius = SmallRadius;
                }
            }
        }

        public override void ScaleAbsolute(double a_scale)
        {
            SmallRadius *= a_scale;
            BigRadius *= a_scale;

            base.ScaleAbsolute(a_scale);
        }
    }
}